<?php

/**
 * 读取配置
 *
 * @param string $key 配置项的键名，格式为'配置文件名.配置项名'
 * @return mixed|null 返回配置项的值或整个配置文件数组（如果未指定配置项名）
 * @throws Exception 如果配置文件不存在、格式不正确或存在安全问题
 */
function config($key)
{
    // 分解键名和配置项
    $parts = explode('.', $key, 2);
    $configFile = basename($parts[0]);
    $configKey = isset($parts[1]) ? $parts[1] : null;

    // 验证键名格式
    if (!preg_match('/^[\w\.]+$/', $key)) {
        throw new Exception("配置密钥格式无效。");
    }

    // 配置文件路径
    $conf_file = ROOT_PATH . "config/{$configFile}.php";
    $realpath = realpath($conf_file);

    // 验证并标准化配置文件路径
    // if (!$realpath || strpos($realpath, ROOT_PATH . 'config') !== 0) {
    //     throw new Exception("无效或不安全的配置文件路径。");
    // }

    // 加载并验证配置文件
    if (!file_exists($realpath) || !is_readable($realpath)) {
        throw new Exception("配置文件不存在或不可读。");
    }
    $conf = include $realpath;
    if (!is_array($conf)) {
        throw new Exception("配置文件格式不正确，必须返回一个数组。");
    }

    // 返回整个配置文件或特定配置项
    return $configKey ? ($conf[$configKey] ?? null) : $conf;
}

/**
 * 获取一个数据表操作对象
 *
 * @param  string $table  数据表名称
 * @param  string $item   默认 db , 对应的数据库一级2配置名称
 * @return object         数据库操作对象
 */
function db(string $table, string $item = 'db'): object
{
    $conf = config($item);
    return Db::getInstance($conf, $table, $item);
}

/**
 * 功能 : 获取一个模型
 *
 * @param  string $modelName  模型名称
 * @return object             模型对象
 * @throws Exception         如果模型不存在或文件名不合法
 */
function model(string $name): object
{
    $model_file = APP_PATH . '/models/' . ucfirst($name) . '.php';

    // 确保模型文件存在
    if (!file_exists($model_file)) {
        throw new Exception("模型文件 {$model_file} 不存在");
    }

    require_once $model_file;

    $model_class = '\\app\\models\\' . ucfirst($name);

    if (!class_exists($model_class)) {
        throw new Exception("模型 {$model_class} 不存在");
    }

    return new $model_class();
}

/**
 * 加载视图
 */
function view(string $view, array $data = [])
{
    // 确保配置正确加载
    // $config = config('view');
    // if (!$config) {
    //     throw new Exception('视图配置加载失败。');
    // }

    $template = new Blade();

    return $template->render($view, $data);
}



/**
 * 获取特定的 url 段
 *
 * @param int $num 要检索的段编号。
 */
function segment(int $num)
{
    $segments = SEGMENTS;

    $value = $segments[$num] ?? '';

    return escape($value);
}

/**
 * 输出 json
 *
 * @param string $msg    提示内容
 * @param string $status 状态，默认是错误，还可设置为 'success'
 */
function json(string $msg, string $status = 'error')
{
    // header('Content-Type: application/json; charset=utf-8');
    echo json_encode(
        [
            'message' => escape($msg),
            'status'  => escape($status)
        ],
        JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE
    );
}

/**
 * 对指定的 URL 执行 HTTP 重定向。
 *
 * @param  string $target_url 重定向应指向的URL
 * @param  int  $status  HTTP状态码，默认为302（临时重定向）
 */
function redirect(string $target_url, int $status = 302)
{
    // 确保没有输出到浏览器，否则重定向将不会生效
    if (headers_sent()) {
        exit("HTTP headers have already been sent.");
    }

    $clean_url  = filter_var($target_url, FILTER_SANITIZE_URL);

    // 验证清理后的URL是否有效，如果不是有效的URL，则抛出异常或进行其他错误处理
    if (!$clean_url || filter_var($clean_url, FILTER_VALIDATE_URL) === false) {
        throw new InvalidArgumentException('Invalid URL provided for redirection.');
    }

    // 防止可能的HTTP响应拆分攻击，确保没有换行符
    $safe_url = str_replace(["\n", "\r"], '', $clean_url);

    // 设置 HTTP 状态码并执行重定向
    http_response_code($status);
    header('Location: ' . $safe_url, true, $status);
    exit();
}

function url_full()
{
    $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http');
    $host = escape($_SERVER['HTTP_HOST']);

    return $protocol . '://' . $host;
}

/**
 * 获取上一页的 URL（如果可用）。
 *
 * @return string 上一页 URL，如果不存在则返回空字符串
 */
function url_previous()
{
    $url = filter_var(escape($_SERVER['HTTP_REFERER']) ?? '', FILTER_VALIDATE_URL);

    // 如果 URL 验证通过，返回 URL，否则返回空字符串
    return $url ?? '';
}

/**
 * 获取当前页 URL
 *
 * @return string 当前页 URL
 */
function url_current()
{
    $request_uri = filter_var(escape($_SERVER['REQUEST_URI']), FILTER_SANITIZE_URL);

    return url_full() . $request_uri;
}

/**
 * 生成网址
 */
function url(string $path = ''): string
{
    return url_full() . filter_var($path, FILTER_SANITIZE_URL);
}

/**
 * 获取请求数据，默认已过滤 html 标签等。
 *
 * @param  string $field_name 请求字段名
 * @param  bool   $sanitize 是否进行数据清理，默认为 true
 * @return mixed  请求的数据
 */
function post($field_name, $sanitize = true)
{
    if (!isset($_POST[$field_name])) {
        return null;
    }

    $value = $_POST[$field_name];

    if ($sanitize) {
        $value = escape($value);
    }

    return $value;
}



/**
 * 获取或者设置会话
 *
 * @param string $key   会话键
 * @param mixed  $value 会话值，如果为null则获取键的值
 *
 * @return mixed|null 如果设置值，返回设置的值；如果获取值且值存在，返回该值；否则返回null
 */

function session(string $key, $value = null)
{
    if (session_status() === PHP_SESSION_NONE) {
        session_start();
    }

    if ($value !== null) {
        // 设置会话值并返回
        $_SESSION[$key] = $value;
        return true; // 设置成功返回true
    }

    // 获取会话值，如果不存在则返回null
    return $_SESSION[$key] ?? null;
}

/**
 * 从会话中删除指定的数据项
 *
 * @param string $key 会话中的键名
 * @return bool 如果键存在并且成功删除则返回 true，否则返回 false
 */
function session_forget(string $key): bool
{
    if (session_status() === PHP_SESSION_NONE) {
        session_start();
    }

    if (isset($_SESSION[$key])) {
        unset($_SESSION[$key]);
        return true; // 成功删除
    }

    return false; // 键不存在，未执行删除操作
}

/**
 * 生成 csrf token
 */
function csrf_token(int $length = 32)
{
    $token = bin2hex(random_bytes($length));
    if (!isset($_SESSION['csrf_token'])) {
        session('csrf_token', $token);
    }

    return $token;
}

/**
 * 获取 csrf token
 */
function get_token()
{
    $token = post('csrf_token');
    session_forget('csrf_token');

    return $token;
}

/**
 * 闪存消息
 *
 * @param string $type (danger, warning, info, success)
 */
function flash(string $type = '', string $message = '')
{
    // 初始化会话
    if (!isset($_SESSION['flash_messages'])) {
        $_SESSION['flash_messages'] = [];
    }

    // 添加消息
    if ($type !== '' && $message !== '') {
        // add the message to the session
        $_SESSION['flash_messages'][$type] = [
            'type'    => escape($type),
            'message' => escape($message),
        ];
        // 显示特定类型的消息
    } elseif ($type !== '' && $message === '') {
        if (isset($_SESSION['flash_messages'][$type])) {
            $flash_message = $_SESSION['flash_messages'][$type];
            unset($_SESSION['flash_messages'][$type]);
            $flash_message = sprintf(
                '<div class="d-flex justify-content-center align-items-center w-100"><div class="alert alert-%s shadow-sm"><span class="alert-text">%s</span></div></div>',
                escape($flash_message['type']),
                escape($flash_message['message'])
            );
            echo $flash_message;
        }
        // 显示所有消息
    } elseif ($type === '' && $message === '') {
        foreach ($_SESSION['flash_messages'] as $type => $flash_message) {
            $flash_message = sprintf(
                '<div class="d-flex justify-content-center align-items-center w-100"><div class="alert alert-%s shadow-sm"><span class="alert-text">%s</span></div></div>',
                escape($flash_message['type']),
                escape($flash_message['message'])
            );
            echo $flash_message;
        }
        // 删除所有消息
        unset($_SESSION['flash_messages']);
    }
}

/**
 * 获取请求IP
 *
 * @return string 客户端的IP地址
 */
function get_ip()
{
    // 定义一个默认的IP地址
    $ip = '127.0.0.1';

    // 检查HTTP_CLIENT_IP是否设置并且不为unknown
    if (!empty($_SERVER['HTTP_CLIENT_IP']) && $_SERVER['HTTP_CLIENT_IP'] !== 'unknown') {
        $ip = $_SERVER['HTTP_CLIENT_IP'];
    }
    // 检查HTTP_X_FORWARDED_FOR是否设置并且不为unknown
    elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR'] !== 'unknown') {
        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
    }
    // 检查REMOTE_ADDR是否设置并且不为unknown
    elseif (!empty($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] !== 'unknown') {
        $ip = $_SERVER['REMOTE_ADDR'];
    }

    // 使用过滤函数来过滤IP地址，确保只获取合法的IP地址
    $filtered_ip = filter_var($ip, FILTER_VALIDATE_IP);

    // 如果获取到的IP地址合法，则返回该IP地址，否则返回默认的IP地址
    return $filtered_ip ? $filtered_ip : '127.0.0.1';
}


/**
 * 消毒数据
 *
 * @param  string $value
 *
 * @return string
 */
function escape(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

/**
 * 可能适用于筛选通过文本区域提交的数据
 *
 * @param string $string      要过滤的字符串
 * @param bool   $strip_tags  是否删除HTML和PHP标记
 * @param string $allowed_tags 允许的HTML标签，多个标签之间用逗号分隔
 * @param bool   $html        是否将特殊字符转换为HTML实体
 *
 * @return string 过滤后的字符串
 */
function filter_string($string, $html = true, $strip_tags = true, $allowed_tags = '')
{
    // 删除字符串首尾处的空白字符（或者其他字符）
    $string = trim($string);
    // 将多个连续空格转换为一个空格，换行除外
    $string = preg_replace('#[^\S\r\n]+#', ' ', $string);
    // 替换换行符为 <br>
    $string = nl2br($string);

    if ($html) {
        // 将特殊字符转换为 HTML 实体
        $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }

    if ($strip_tags) {
        // 删除HTML和PHP标记
        $string = strip_tags($string, $allowed_tags);
    }

    return $string;
}

/**
 * 去除字符串中所有的空白字符
 *
 * @param string $str 需要处理的字符串
 * @return string 处理后的字符串，其中所有空白字符都已被移除
 */
function remove_all_whitespace($str)
{
    // 使用正则表达式匹配所有空白字符（包括空格、制表符、换行符等）并替换为空字符串
    return preg_replace('/\s+/u', '', $str);
}

/**
 * 生成邀请码
 */
function get_invite_code(int $length = 8): string
{
    $codeAlphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    $max = strlen($codeAlphabet) - 1;
    $token = '';
    for ($i = 0; $i < $length; ++$i) {
        $randomIndex = random_int(0, $max);
        $token .= $codeAlphabet[$randomIndex];
    }

    return $token;
}

/**
 * 从环境配置文件中获取指定键的值
 * 如果配置项不存在，则返回默认值
 * 
 * @param string $key     需要获取的环境变量的键名。
 * @param mixed  $default 当指定的键不存在时返回的默认值，默认为null。
 */
function env(string $key, $default = null)
{
    $file = ROOT_PATH . '.env1';

    if (!file_exists($file)) {
        throw new Debug("环境配置文件不存在。", 1);
    }

    if (!is_readable($file)) {
        throw new Debug("环境配置文件不可读。", 1);
    }

    // 读取并解析 .env 文件
    $env = [];
    $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($lines as $line) {
        if (strpos($line, '=') !== false) {
            list($k, $v) = explode('=', $line, 2);
            $env[trim($k)] = trim($v);
        }
    }

    // 检查配置项是否存在
    if (!array_key_exists($key, $env)) {
        return $default;
    }

    // 返回配置项的值
    return $env[$key];
}

/**
 * 友好的时间显示
 */
function nice_time($time)
{
    $timeDiff = time() - $time;

    // 时间单位（秒）和对应的描述
    $units = [
        31536000 => ' 年',
        2592000  => ' 个月',
        604800   => ' 周',
        86400    => ' 天',
        3600     => ' 小时',
        60       => ' 分钟',
        1        => ' 秒'
    ];

    foreach ($units as $seconds => $unit) {
        if ($timeDiff >= $seconds) {
            $count = floor($timeDiff / $seconds);
            return $count . $unit;
        }
    }

    // 如果由于某种原因没有返回任何值，可以添加一个默认返回值
    return '<span class="time-text">未知时间</span>';
}

/**
 * 格式化文件大小
 *
 * @param  int|float $size 文件大小（字节）
 * @param  int $precision 保留的小数位数
 * @return string 格式化的文件大小
 */
function format_size($size, $precision = 2)
{
    // 定义单位数组
    $units = [' 字节', ' KB', ' MB', ' GB', ' TB'];

    // 通过除以 1024 来找到合适的单位
    for ($i = 0; $size > 1024 && $i < 4; $i++) {
        $size /= 1024;
    }

    // 返回格式化的文件大小
    return round($size, $precision) . $units[$i];
}

/**
 * 转换24小时制的时间为上午/下午格式
 */
function convertTimeToAMPM($timestamp)
{
    $time = date('g:i', $timestamp);
    $period = date('a', $timestamp);
    $date = date('Y年m月d日', $timestamp);

    $formattedTime = str_replace(array('am', 'pm'), array('上午', '下午'), $period) . $time;
    $formattedDateTime = $formattedTime . ' · ' . $date;

    return $formattedDateTime;
}

/**
 * 生成一个指定长度的唯一ID（类似于UUID）
 *
 * @param int $length 生成ID的长度，默认为32
 * @return string 返回生成的唯一ID字符串
 */
function uuid(int $length = 32): string
{
    $bytes = random_bytes((int)ceil($length / 2));
    return substr(bin2hex($bytes), 0, $length);
}

/**
 * @param int    $code     HTTP状态码
 * @param string $message  自定义错误信息，可选
 */
function abort(int $code, string $message = '')
{
    // 有效的HTTP状态码列表
    $validCodes = [404, 403]; // 可以根据需要扩展

    // 验证状态码是否有效
    if (!in_array($code, $validCodes)) {
        // 处理无效的状态码，例如记录日志、抛出异常或返回默认错误页面
        // 这里为了简单起见，我们默认使用400错误码作为回退
        $message = '页面不存在 或者 无访问权限。';
    }

    // 自定义或默认的错误信息
    $defaultMessages = [
        403 => '无访问权限。',
        404 => '页面不存在。',
    ];

    $message = $message ?: $defaultMessages[$code] ?? '未知错误。';

    // 发送HTTP响应码
    http_response_code($code);

    // 包含统一的错误处理模板，并传递错误代码和消息
    require CORE_PATH . "/templates/error.php";

    exit(); // 确保在输出错误页面后终止脚本执行
}

/**
 * 检查字符串是否以给定的子字符串开头。
 *
 * @param string $haystack 要搜索的字符串。
 * @param string|array $needle 要搜索的子字符串。您也可以使用字符串数组。
 * @return bool
 */
function start_with(string $haystack, $needle): bool
{
    $result = false;
    foreach ((array)$needle as $item) {
        if ($result) break;
        $length = mb_strlen($item);
        $result = mb_substr($haystack, 0, $length) == $item;
    }
    return $result;
}

/**
 * 检查两个值是否相等，用于确定 radio 或 checkbox 的选中状态。
 *
 * @param string $val1 比对值1
 * @param string $val2 比对值2
 * @return string 如果两个值相等，则返回 "checked"，否则返回空字符串。
 */
function is_checked(string $val1, string $val2): string
{
    return $val1 === $val2 ? "checked" : "";
}

/**
 * 检查选项值是否应被选中，并返回相应的 HTML 属性字符串。
 *
 * @param string $optionValue   当前选项的值。
 * @param string $compareValue  要比较的值。
 * @return string              如果选项值匹配比较值，则返回 'selected="selected"'，否则返回空字符串。
 */
function is_option_selected(string $optionValue, string $compareValue): string
{
    return $optionValue === $compareValue ? ' selected="selected"' : '';
}

/**
 * 规划缓存命名
 *
 * @param  string  $name      缓存名称
 * @param  mixed   $parameter 缓存影响参数
 * @param  boolean $isSuper   是否为全局缓存
 * @return string 缓存名称
 */
function set_cache_name($name, $parameter = '', $isSuper = true)
{
    $cacheConfig = config('cache');
    $parameter   = is_array($parameter) ? implode('_', $parameter) : $parameter;
    $cacheName   = $isSuper ? $cacheConfig['prefix'] . $name . $parameter : $cacheConfig['prefix'] . 'CONTROLLER_NAME' . '_' . 'METHOD_NAME' . '_' . $name . $parameter;
    if (empty($cacheConfig['name2md5'])) {
        return $cacheName;
    }
    return md5($cacheName);
}

/**
 * 写入配置文件
 *
 * @param string $item      配置项，使用文件名区分
 * @param array  $envValues 环境变量值数组，用于替换配置中的默认值
 * @throws Exception 如果文件无法写入或路径无效
 */
function write_config(string $configItem, array $envValues)
{
    // 配置项
    $configData = config($configItem);

    // 替换配置项中的值，如果环境变量有对应值，则使用环境变量的值
    foreach ($configData as $key => &$defaultValue) {
        if (array_key_exists($key, $envValues)) {
            // 可以在这里添加验证逻辑，以确保$envValues[$key]是安全的
            $defaultValue = $envValues[$key];
        }
    }
    unset($defaultValue); // 取消引用，避免潜在的问题

    // 生成配置文件内容
    $configContent = "<?php\nreturn " . var_export($configData, true) . ";\n";
    $configFile    = ROOT_PATH . 'config/' . $configItem . '.php';

    // 写入配置文件
    $written = file_put_contents($configFile, $configContent);
    if ($written === false) {
        throw new Exception("无法写入配置文件: " . $configItem);
    }
}

/**
 * 将URL转换为链接标签
 * @param string $url 含URL的字符串
 * @param array  $protocols 要转换的协议, http/https, ftp/ftps, mail
 * @param string $target 是否新页面打开:_blank,_self
 * @return string
 */
function url2Link(string $url, array $protocols = ['http', 'https'], string $target = '_blank'): string
{
    if (!empty($url)) {
        if (!empty(array_intersect($protocols, ['http', 'https']))) {
            $pattern = '@(http(s)?)?(://)?(([a-zA-Z])([-\w]+\.)+([^\s\.]+[^\s]*)+[^,.\s])@i';
            $url     = preg_replace($pattern, "<a href=\"http$2://$4\" rel=“nofollow” target=\"{$target}\">$0</a>", $url);
        }

        if (!empty(array_intersect($protocols, ['ftp', 'ftps']))) {
            $pattern = '/(ftp|ftps)\:\/\/[-a-zA-Z0-9@:%_+.~#?&\/=]+(\/\S*)?/i';
            $url     = preg_replace($pattern, "<a href=\"$0\" rel=“nofollow” target=\"{$target}\">$0</a>", $url);
        }

        if (in_array('mail', $protocols)) {
            $pattern = '/([^\s<]+?@[^\s<]+?\.[^\s<]+)(?<![\.,:])/';
            $url     = preg_replace($pattern, "<a href=\"mailto:$0\" rel=“nofollow” target=\"{$target}\">$0</a>", $url);
        }
    }

    return $url;
}


/**
 * 调试输出
 */
function dump(...$value): void
{
    ob_start();
    var_dump(...$value);
    $output = ob_get_clean();
    $output = preg_replace('/]=>\n(\s+)/m', '] => ', $output);
    $output = '<pre>' . htmlspecialchars($output, ENT_SUBSTITUTE) . '</pre>';
    echo $output;
}

/**
 * 输出并退出
 */
function dd($value)
{
    $backtrace = debug_backtrace();

    if (isset($backtrace[0])) {
        echo '文件: ' . $backtrace[0]['file'] . '<br>';
        echo '行号: ' . $backtrace[0]['line'] . '<hr>';
    }

    if (isset($backtrace[1])) {
        echo '类：' . $backtrace[1]['class'] . '<br>';
        echo '方法：' . $backtrace[1]['function'] . '<br><br>';
    }

    echo "类型: <b>" . gettype($value) . "</b><br>";
    dump($value);
    echo '<a href="javascript:history.go(-1)">返回</a>';
    exit();
}
